Skip to content

Conversation

@shaneeza
Copy link
Collaborator

@shaneeza shaneeza commented Dec 15, 2025

✍️ Proposed changes

🎟 Jira ticket: Name of ticket

This PR introduces utility functions for managing time input segment state, including validation, formatting, and conversion between 12-hour and 24-hour formats. The implementation handles timezone conversions, segment validation, and provides comprehensive test coverage.

This PR is the third PR in a chain of PRs

  1. [LG-5538] feat(time-input): Parse time #3378
  2. [LG-5532] feat(time-input) display segment values #3379
  3. This PR
  4. [LG-5532] feat(time-input): Segment state #3386
  5. [LG-5532] feat(time-input): Segment misc #3387

🧪 How to test changes

…cale dependency and enhancing formatting logic
…kMode and size, and add console log for debugging in TimeInputInputs
…omponent, covering rendering, value updates, and keyboard interactions
…omponent, covering rendering, value updates, and keyboard interactions
…12HourFormat prop, replacing TimeSegments with TimeSegment for improved clarity and consistency
…db/leafygreen-ui into LG-5538/segments-parse-time
…ygreen-ui into LG-5532/segments-display-values
@shaneeza shaneeza requested a review from tsck December 23, 2025 15:29
@shaneeza shaneeza removed the request for review from tsck December 23, 2025 15:44
Copy link
Collaborator

@stephl3 stephl3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

finished reviewing the latter half of the PR, but still reviewing more 🫡

Comment on lines 7 to 8
import { isEverySegmentFilled } from '../isEverySegmentFilled/isEverySegmentFilled';
import { isEverySegmentValueExplicit } from '../isEverySegmentValueExplicit/isEverySegmentValueExplicit';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { isEverySegmentFilled } from '../isEverySegmentFilled/isEverySegmentFilled';
import { isEverySegmentValueExplicit } from '../isEverySegmentValueExplicit/isEverySegmentValueExplicit';
import { isEverySegmentFilled } from '../isEverySegmentFilled';
import { isEverySegmentValueExplicit } from '../isEverySegmentValueExplicit';

Comment on lines 11 to 19
export const isEverySegmentFilled = (segments: TimeSegmentsState) => {
const isEverySegmentFilled = Object.values(segments).every(segment => {
const isSegmentDefined = isDefined(segment);
const isSegmentEmpty = segment === '';
return !isSegmentEmpty && isSegmentDefined;
});
// check if all segments are not empty
return isEverySegmentFilled;
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The values of an object that has a type of TimeSegmentsState will always be strings, so this could be simplified to:
const isEverySegmentFilled = Object.values(segments).every(segment => segment !== '');

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also worth considering if it's still necessary to have a util at that point

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to keep this for now and reevaulate in the next PR in this chain that uses it

Comment on lines 24 to 25
segment: segment as TimeSegment,
value: value as string,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these type assertions necessary?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh I see Object.entries() is expanding the types to be strings. I think it'd make more sense to move the assertion to that line so it propagates to these later lines. something like:
(Object.entries(segments) as Array<[TimeSegment, string]>)

Comment on lines 26 to 27
defaultMin: getDefaultMin({ is12HourFormat })[segment as TimeSegment],
defaultMax: getDefaultMax({ is12HourFormat })[segment as TimeSegment],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can optimize this by creating the initial objects for defaultMinValues and defaultMaxValues outside of the loop

segments: TimeSegmentsState;
is12HourFormat: boolean;
}): boolean => {
return Object.entries(segments).every(([segment, value]) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar note as above about asserting types when using Object.entries()

import { TimeSegment, TimeSegmentsState } from '../../shared.types';
import { getTimeSegmentRules } from '../getTimeSegmentRules';

export const isExplicitSegmentValue = (is12HourFormat: boolean) =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of this helper sounds like it would return a boolean. Maybe createExplicitTimeSegmentValidator is more appropriate?

Comment on lines 16 to 18
describe('hour', () => {
describe('returns false', () => {
test('if hour is 0', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could accept the mild duplication trade-off and flatten this for readability

Suggested change
describe('hour', () => {
describe('returns false', () => {
test('if hour is 0', () => {
describe('hour segment', () => {
test('returns false when hour is 0', () => {

Comment on lines 38 to 40
describe('returns true', () => {
describe('when single digit and value is greater than or equal to the min explicit value (2)', () => {
test.each(range(2, 10))('%i', i => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly for this:

Suggested change
describe('returns true', () => {
describe('when single digit and value is greater than or equal to the min explicit value (2)', () => {
test.each(range(2, 10))('%i', i => {
test.each('returns true for single digit hour %i', () => {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't continue repeating feedback but similarly for the later blocks, I think it would help readability to flatten or block these with conditions or labels as opposed to assertion statements

Comment on lines 36 to 37
// If the date is valid and all segments are explicit, then the value should be set.
const isValidDateAndSegmentsAreExplicit =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could just be me, but comments like this seem redundant because it re-states the variable name in sentence format

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense

…TimeSegmentValidator for clarity and update its usage in isEverySegmentValueExplicit
export { isSameTZDay } from './isSameTZDay';
export { isSameTZMonth } from './isSameTZMonth';
export { isSameUTCDay } from './isSameUTCDay';
export { isSameUTCDayAndTime } from './isSameUTCDayAndTime';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haven't been looking for it for time-input but do changes to date-utils need a changeset?

Copy link
Collaborator Author

@shaneeza shaneeza Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes! I was planning on adding all changesets related to the segment integration in a separate PR

import { isSameUTCDay } from '.';

const TZOffset = -5;
const timeZone = 'America/New_York';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: would more explicitly name this constant

Comment on lines 34 to 41
// September 9, 2023 21:00 EDT
const local = newTZDate({
timeZone,
year: 2023,
month: 8,
date: 9,
hours: 21,
}); // September 10, 2023 01:00 UTC
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while making these changes to these tests, it could be helpful to rewrite the test statements because they're quite confusing currently since they just say "returns true" and "returns false"

describe('packages/date-utils/isSameUTCMonth', () => {
beforeEach(() => {
mockTimeZone('America/New_York', TZOffset);
mockTimeZone(timeZone, TZOffset);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly, for these tests, it could be beneficial to add context to the test statements that currently only state boolean values

* @param day2 - The second date to check
* @returns Whether the two dates are the same day and time in UTC
*/
export const isSameUTCDayAndTime = (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe isSameUTCDateTime since we're comparing DateType?

*/
const { selectUnit, setSelectUnit } = useSelectUnit({
dayPeriod: timeParts.dayPeriod,
dayPeriod: timeParts.dayPeriod as DayPeriod,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand most date time parts being string types since they are input fields, but have you considered making dayPeriod strictly a DayPeriod so you don't have to include this type assertion?

* @param segments - The segments to check
* @returns Whether some segment exists
*/
export const doesSomeSegmentExist = (segments: TimeSegmentsState) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates isEverySegmentFilled at this point, so we should consolidate

option => option.displayName === dayPeriod,
);

return selectUnitOption ?? unitOptions[0];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason why it it's possibly not found? feels like there's something else to update to prevent that from being possible

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array.find() return type is T | undefined even though we know that it shouldn't return undefined so we're just adding a fallback.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we update the type of UnitOptions to use DayPeriod for it's object values, it should be safe to assert that a value is always found and simplify this to:
return unitOptions.find(option => option.displayName === dayPeriod)!;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is true. In this case, it will always be one of the options.

Comment on lines 29 to 33
dateValues: {
day: string;
month: string;
year: string;
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there an existing type for this?

Comment on lines 43 to 71
/**
* Check if all segments are filled and valid (not necessarily explicit). If they are, return the UTC date.
*/
if (
isEverySegmentFilled(segments) &&
isEverySegmentValid({ segments, is12HourFormat })
) {
return newTZDate({
year: Number(year),
month: Number(month) - 1,
date: Number(day),
hours: Number(converted12hTo24hHour),
minutes: Number(minute),
seconds: Number(second),
timeZone,
});
}

/**
* Check if any segments are filled. If not, return null. This means all segments are empty.
*/
if (!doesSomeSegmentExist(segments)) {
return null;
}

/**
* Return an invalid date object if some segments are empty or invalid.
*/
return new Date('invalid');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since isEverySegmentFilled and doesSomeSegmentExist are the same, I think this can be approached differently. Also, I think it will be more readable if some condition leads to an early return of an invalid date, and the final return is return newTZDate(...);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're different. isEverySegmentFilled checks that all segments are filled, while doesSomeSegmentExist checks if at least one segment is filled.

We need to check if all segments are empty. If they are, then we return null, so we use doesSomeSegmentExist for this check. It will return true if at least one segment is filled. We can't use isEverySegmentFilled because it will return false if at least one segment is empty. There is a chance that the other segments can be filled.

We also need to check if every segment is filled. That's when we use isEverySegmentFilled. We can't use doesSomeSegmentExist because that will return true as soon as one segment is filled. So there is a chance that the other segments can be empty.

I get confused easily with every and some, so I hope this explanation makes sense. If not, happy to jump on a call.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yeah, I see that they use a different method. My b!

…ce getNewUTCDateFromSegments logic for better clarity
…and adjust related logic in TimeInputInputs and getFormattedDateTimeParts
…t handling and update getNewUTCDateFromSegments to utilize it
…proving comments and consistency in date conversions
…and isSameUTCMonth.spec for better clarity and consistency
@shaneeza shaneeza requested a review from stephl3 December 30, 2025 00:08
option => option.displayName === dayPeriod,
);

return selectUnitOption ?? unitOptions[0];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we update the type of UnitOptions to use DayPeriod for it's object values, it should be safe to assert that a value is always found and simplify this to:
return unitOptions.find(option => option.displayName === dayPeriod)!;

[DateTimePartKeys.dayPeriod]: DayPeriod;
};

export type DateParts = Pick<DateTimeParts, 'day' | 'month' | 'year'>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating another DateParts type, I was actually wondering if there was an existing one in one of the other date/time-related packages. It looks like there is DateSegment but it's in the date-picker package. It'd be ideal to centralize some types in a shared package like date-utils or input-box. It would keep things more DRY

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I like this idea, but I want to think it through a little more on where it should be placed and what should be included. I can create a ticket for this.

Comment on lines 43 to 71
/**
* Check if all segments are filled and valid (not necessarily explicit). If they are, return the UTC date.
*/
if (
isEverySegmentFilled(segments) &&
isEverySegmentValid({ segments, is12HourFormat })
) {
return newTZDate({
year: Number(year),
month: Number(month) - 1,
date: Number(day),
hours: Number(converted12hTo24hHour),
minutes: Number(minute),
seconds: Number(second),
timeZone,
});
}

/**
* Check if any segments are filled. If not, return null. This means all segments are empty.
*/
if (!doesSomeSegmentExist(segments)) {
return null;
}

/**
* Return an invalid date object if some segments are empty or invalid.
*/
return new Date('invalid');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yeah, I see that they use a different method. My b!

@shaneeza shaneeza requested a review from stephl3 December 30, 2025 22:09
@github-actions
Copy link
Contributor

Coverage after merging LG-5532/segments-state-utils into shaneeza/segment-logic-integration will be

79.22%

Coverage Report for Changed Files
FileStmtsBranchesFuncsLinesUncovered Lines
packages/date-utils/src/isSameUTCDateTime
   isSameUTCDateTime.ts100%100%100%100%
packages/date-utils/src/newTZDate
   newTZDate.ts85.71%71.43%100%100%36, 58
packages/time-input/src
   constants.ts100%100%100%100%
packages/time-input/src/TimeInputInputs
   TimeInputInputs.tsx0%0%0%0%19, 21–22, 24–25, 31, 37, 43, 53, 59, 68, 72, 72, 76, 86
packages/time-input/src/TimeInputSelect
   TimeInputSelect.tsx94.44%50%100%100%38
packages/time-input/src/hooks/useSelectUnit
   useSelectUnit.ts0%0%0%0%19, 23–24, 26, 37, 46–47, 49, 51, 51, 51–53, 57
packages/time-input/src/utils/convert12hTo24h
   convert12hTo24h.ts100%100%100%100%
packages/time-input/src/utils/doesSomeSegmentExist
   doesSomeSegmentExist.ts100%100%100%100%
packages/time-input/src/utils/getFormatPartsValues/getFormattedDateTimeParts
   getFormattedDateTimeParts.ts93.33%75%100%100%43
packages/time-input/src/utils/getNewUTCDateFromSegments
   getNewUTCDateFromSegments.ts100%100%100%100%
packages/time-input/src/utils/getPaddedTimeSegmentsFromDate
   getPaddedTimeSegmentsFromDate.ts100%100%100%100%
packages/time-input/src/utils/getPaddedTimeSegmentsFromDate/getPaddedTimeSegments
   getPaddedTimeSegments.ts100%100%100%100%
packages/time-input/src/utils/isEverySegmentValueExplicit
   isEverySegmentValueExplicit.ts100%100%100%100%
packages/time-input/src/utils/shouldSetValue
   shouldSetValue.ts100%100%100%100%

@shaneeza shaneeza merged commit 753fca9 into shaneeza/segment-logic-integration Dec 31, 2025
12 of 14 checks passed
@shaneeza shaneeza deleted the LG-5532/segments-state-utils branch December 31, 2025 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants